Slient Blog

Java线程安全问题

2017-04-14

多线程的安全问题

1
2
3
4
5
6
7
8
9
10
11
12
class Ticket implements Runnable{
private int ticket =100;
public void run(){
while(true){
if(ticket > 0){
System.out.System.out.println(ticket --);
}
}
}
}

问题的原因

当多个线程同时获取获取cpu执行权的时候,都会执行ticket–;多条语句在操作同一个线程的时候,一个线程堆多条语句只执行了一部分,还没有执行完,另一个线程参与进来,导致共享数据

解决办法

对于多条操作共享的数据的时候,只能让一个线程执行完,其他线程不可以参加执行

java对于多线程的安全问题提供了解决方案

就是同步代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
synchronized(对象){
需要被同步的代码
}
ReentrantLock lock = new ReentrantLock();
void test(){
lock.lock();//如果被其他锁自愿锁定,会在这里等待锁释放,达到暂停的效果
try {
//操作
}finally {
lock.unlock();//释放锁
}
}

对象(监视器,锁)

判断的条件,当线程获得执行权之后执行同步代码,进入之后讲对象关闭。直到上一个线程执行完同步代码。对象打开,这个时候各个线程又开始争取执行权。eg:厕所的门把手;改之后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Ticket implements Runnable{
private int ticket =100;
Object obj=new Object();
public void run(){
while(true){
synchronized (obj) {
if(ticket>0){
System.out.System.out.println(ticket--);
}
}
}
}
}
public class TicketTest implements Runnable {
int num = 20;
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
if (num > 0){
System.out.println("剩余票数 :"+Thread.currentThread()+" "+ num--);
}
lock.unlock();
}
}
public static void main(String[] args) {
TicketTest ticketTest = new TicketTest();
Thread thread1 = new Thread(ticketTest);
Thread thread2 = new Thread(ticketTest);
Thread thread3 = new Thread(ticketTest);
thread1.start();
thread2.start();
thread3.start();
}
}

Lock接口的主要方法

  • void lock()执行这个方法是,如果锁处于空闲状态,当前线程就会获取锁,如果锁已经被其他线程获取,会禁止当前线程,知道当前线程获取到资源
  • boolean tryLock()如果锁可以用,就可以获取锁,并且立刻返回true,否则返回false,该方法和lock的区别是,tryLock只是尝试获取锁,如果锁不可以用,不会导致当前线程被禁用,会继续下去执行代码。而lock方法是一定要获取当前锁
  • void unlock()执行此方法时候,当前线程将会释放拥有的锁。
  • Condition newCondition 获取等待通知的组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能使用该组件的awit方法,调用之后,当前线程释放锁

同步的前提:

  • 必须要有两个或者两个以上的线程
  • 必须多个线程使用同一个锁
  • 必须保证同步中有一个线程在运行

好处 :解决多线程同步问题

弊端 :每次都在判定锁,消耗了资源

重入锁

当一个线程得到一个对象后再次请求该对象锁时候是可以再次得到该对象的锁的(Synchronized,Lock)都是可以重入的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class XLock {
ReentrantLock lock = new ReentrantLock();
void test(){
lock.lock();//如果被其他锁自愿锁定,会在这里等待锁释放,达到暂停的效果
try {
test1();
}finally {
lock.unlock();//释放锁
}
}
void test1(){
lock.lock();
try {
System.out.println("hello test1");
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
new XLock().test1();
}
}

公平锁

cpu在调度线程的时候是在等待队列里面随机挑选一个线程,由于这种随机性是无法保证先到先得的。但这种情况就会出现饥饿现象。有些线程可能永远无法获取带cpu资源,优先级高的线程会无限强制性他的资源,那么如何解决这个问题呢,这个就需要使用公平锁了。公平锁可以保证线程按照顺执行按照时间的先后。但是效率比较低,因为需要顺序执行所以需要一个执行队列。ReetrantLock就是一个公平锁,只需要在构造方法中传入true就可以变为公平锁
1
2
3
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

synchronized 和 reentrantLock的比较

区别
  • Lock是一个接口,synchronized是一个关键字;
  • sy在发生异常的时候会自动释放自己占有的锁,因此不会导致死锁现象,但是lock需要我们主动地调用unlock去释放锁
  • lock可以知道自己有没有成功获取锁
  • look可以提高多个线程进行读操作的效率
  • lock可以让等待锁的线程中断,而sy不可以,会让等待锁的线程一直等待下去

    java线程池

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    public class ThreadPoolTest {
    public static void main(String[] args) {
    //线程池的一般使用
    ExecutorService executorService = new ThreadPoolExecutor(
    5,//核心线程数
    10,//最大线程数
    10, //非核心线程的闲置超时时长
    TimeUnit.SECONDS, //时间单位
    new LinkedBlockingQueue<>());//放置线程的队列
    //没有返回值的提交方式
    executorService.execute(
    () -> System.out.println("this is execute ")
    );
    //有返回值的提交方式,我们可以通过这个返回值来判断任务是否执行成功
    Future<Integer>integerFuture = executorService.submit(
    () -> 2
    );
    Future<String>integerFuture1 = executorService.submit(
    () -> System.out.println("do submit by runnable"),
    "success"
    );
    try {
    if (integerFuture.get() == 2) {
    System.out.println("success do submit by callable");
    }
    if (integerFuture1.get().equals("success")){
    System.out.println("success do submit by runnable");
    }
    } catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
    }
    //结束线程池中的所有任务不管有没有执行完成
    executorService.shutdownNow();
    //线程池的工作流程
    //1 提交任务后,如果线程池中的线程数量没有达到核心线程的数量,开启一个核心线程来执行任务‘
    //2 如果达到了核心线程的数量,把当前任务放到任务队列中等待核心线程
    //3 如果任务队列已经满了,无法将任务插入到任务队列中,则开启一个非核心线程去执行任务
    //4 如果核心线程的数量已经满了,并且达到了最大线程数,那么就会拒绝执行这个任务,这个时候就会调用,RejectedExecutionHandle中的rejectedExecution方法来通知调用者
    //四种线程池类
    //newFixedThreadPool
    //核心线程数目等于最大线程数目,意味着可以和很快的执行任务,并且使用Linked作为线程队列,大小也不受限制,所有的活跃线程也不会被回收,除非线程池关闭。并且不存在超时限制
    ExecutorService fixPool = Executors.newFixedThreadPool(4);
    // public static ExecutorService newFixedThreadPool(int nThreads) {
    // return new ThreadPoolExecutor(nThreads, nThreads,
    // 0L, TimeUnit.MILLISECONDS,
    // new LinkedBlockingQueue<Runnable>());
    // }
    //newCachedThreadPool
    //我们可以看出核心线程数为0,最大线程数为Integer.MaxValue,现有的线程无法执行新任务,直接创建新的线程去执行,不会去等待,当线程超过60s闲置就会回收,因为是使用SynchronousQueue内部相当于一个空集合
    ExecutorService cachePool = Executors.newCachedThreadPool();
    // public static ExecutorService newCachedThreadPool() {
    // return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    // 60L, TimeUnit.SECONDS,
    // new SynchronousQueue<Runnable>());
    // }
    //newScheduledThreadPool
    ScheduledExecutorService schedulePool = Executors.newScheduledThreadPool(4);
    schedulePool.schedule(() -> System.out.println("延迟三秒之后执行任务"),3,TimeUnit.SECONDS);
    //上一个执行到下一个开始执行消耗的时间,周期性的执行任务
    schedulePool.scheduleAtFixedRate(() -> {
    System.out.println("延迟三秒之后每隔两秒执行一次");
    }, 3, 2, TimeUnit.SECONDS);
    //只有一个核心线程,等待队列无线大,这个时候不需要处理线程同步问题,一个线程处于活跃的时候,其余线程处于等待队列中
    schedulePool.scheduleWithFixedDelay(()-> System.out.println("延迟三秒后每隔两秒执行一次,等上一个执行完"),3,2,TimeUnit.SECONDS);
    ExecutorService singlePool = Executors.newSingleThreadExecutor();
    // public static ExecutorService newSingleThreadExecutor() {
    // return new Executors.FinalizableDelegatedExecutorService
    // (new ThreadPoolExecutor(1, 1,
    // 0L, TimeUnit.MILLISECONDS,
    // new LinkedBlockingQueue<Runnable>()));
    // }
    }
    }

死锁

死锁产生的条件

  • 互斥条件:一个资源每次只能被一个线程使用
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已经获得资源保持不放
  • 不剥夺条件:线程已经获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件: 若干个线程形成一种头尾详解的循环等待资源关系
    在java编程中,有三种典型的死锁类型:静态的锁顺序死锁,动态的锁顺序死锁,协作对象之间发生的死锁。
    ####静态的锁顺序死锁
    a和b两个方法都需要A和B锁,一个线程在执行a方法的时候获得了A锁,在等待B锁的时候;另一个线程执行了b方法已经获得了B锁,在等待A锁。这种状态,就是发生了静态的锁顺序死锁
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class DLock {
    Object A = new Object();
    Object B = new Object();
    public void a(){
    synchronized (A){
    synchronized (B){
    System.out.println("success a");
    }
    }
    }
    public void b(){
    synchronized (B){
    synchronized (A){
    System.out.println("success b");
    }
    }
    }
    }
相同的做法是所有需要多个锁的线程,都要以相同的顺序来获得锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DLock {
Object A = new Object();
Object B = new Object();
public void a(){
synchronized (A){
synchronized (B){
System.out.println("success a");
}
}
}
public void b(){
synchronized (A){
synchronized (B){
System.out.println("success b");
}
}
}
}

动态的锁顺序死锁

动态的锁顺序死锁是指两个线程调用同一个方法时,传入的参数颠倒造成的死锁。如下代码,一个线程调用了transferMoney方法传入参数accountA,accountB,另外一个线程调用了transferMoney方法并传入参数accountB,accountA。此时就可能发生在静态的锁顺序死锁中存在的问题,即:第一个县城获得了accountA锁并且等待accountB锁,第二个线程获得了accountB锁并且等待accountA锁
1
2
3
4
5
6
7
8
9
public class RLock {
public void transefMoney(Object accountA, Object accountB) {
synchronized (accountA) {
synchronized (accountB) {
System.out.println("success");
}
}
}
}
动态的锁解决方案如下:使用System.identifyHashCode来定义锁的顺序。确保所有的线程都以相同的顺序获得锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class RLock {
private Object object = new Object();
public void transefMoney( Object accountA, Object accountB) {
class Helper{
public void transfer(){
System.out.println("success");
}
}
int fromA = System.identityHashCode(accountA);
int fromB = System.identityHashCode(accountB);
if (fromA < fromB ){
synchronized (accountA){
synchronized (accountB){
new Helper().transfer();
}
}
}else if (fromA > fromB){
synchronized (accountB){
synchronized (accountA){
new Helper().transfer();
}
}
}else {
synchronized (object){
synchronized (accountA){
synchronized (accountB){
new Helper().transfer();
}
}
}
}
}
}

协作对象之间发生的死锁

有的时候,死锁没有那么明显,比如两个相互协作的类之间发生死锁,比如下面代码,一个线程调用了Taxi对象的setLocation方法,另一个线程调用了Dispatcher对象的getImage方法。此时可能会发生,第一个线程持有Taxi对象锁并等待Dispatcher对象锁,另一个线程持有Disoatcher对象锁并且等待Taxi对象锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Taxi {
private Point location,destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation(){
return location;
}
public synchronized void setLocation(){
this.location = location;
if (location.equals(destination)){
//调用外部的方法
dispatcher.notifyAvailable(this);
}
}
}
public class Dispatcher {
private final Set<Taxi>taxis;
private final Set<Taxi>availableTaxis;
public Dispatcher(Set<Taxi> taxis, Set<Taxi> availableTaxis) {
this.taxis = taxis;
this.availableTaxis = availableTaxis;
}
public synchronized void notifyAvailable(Taxi taxi){
availableTaxis.add(taxi);
}
public synchronized Image getImage(){
Image image = null;
for (Taxi t : taxis){
//调用外部的方法
System.out.println(t.getLocation());
}
return image;
}
}

以上的代码中,我们在持有锁的情况下调用了外部的代码,而外部的代码也需要一个锁,这是非常危险的。可能发生死锁,为了避免这种情况的发生,我们使用开放调用,如果调用某个外部方法时候不需要持有锁,我们称之为开放调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Taxi {
private Point location,destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation(){
return location;
}
public void setLocation(){
boolean is = false;
synchronized (this){
this.location = location;
is = location.equals(destination);
}
if (is){
//调用外部的方法
dispatcher.notifyAvailable(this);
}
}
}
public class Dispatcher {
private final Set<Taxi>taxis;
private final Set<Taxi>availableTaxis;
public Dispatcher(Set<Taxi> taxis, Set<Taxi> availableTaxis) {
this.taxis = taxis;
this.availableTaxis = availableTaxis;
}
public synchronized void notifyAvailable(Taxi taxi){
availableTaxis.add(taxi);
}
public Image getImage(){
Set<Taxi>copy;
synchronized (this){
//用一些无关操作去进行同步
copy = new HashSet<Taxi>(taxis);
}
Image image = null;
for (Taxi t : copy){
//调用外部的方法
System.out.println(t.getLocation());
}
return image;
}
}
Tags: Java
使用微信添加

若你觉得我的文章对你有帮助,请添加我为好友

扫描二维码,分享此文章